﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using MediaController.Client.DAL;

namespace MediaController.Utils.Listener
{
    /// <summary>
    /// File system folder queue listener.
    /// </summary>
    /// <remarks>
    /// The listener listends from the given input folder queue. if file exist in the
    /// folder the <see cref="FileReceived"/> event is raised with the received file.
    /// With the <see cref="Directory.GetCurrentDirectory"/> the client can read the file and remove
    /// it from the queue. 
    /// <div>Note: if the file is not removed it will be send again, it
    /// acts as a commit to processing the file.</div>
    /// <para>
    /// The listener listens to rooted path only, if non rooted path given it root it
    /// to <see cref="Directory"/>.
    /// </para>
    /// </remarks>
    public class FolderListener : IDisposable
    {
        #region Data members

        /// <summary>
        /// the time in milliseconds to sleep if no files found in the input path before
        /// the next check.
        /// </summary>
        private const int BLOCKING_SLEEP_TIME = 2000;

        /// <summary>
        /// the time in milliseconds to wait until the listener has stoped gracefully.
        /// </summary>
        private const int STOP_TIMEOUT = 30;

        /// <summary>
        /// The directory depth to get files under the pick up folder
        /// </summary>
        private readonly int _directoryDepth;

        /// <summary>
        /// The filter used to get files from the pick up folder
        /// </summary>
        private readonly string _fileFilter;

        /// <summary>
        /// queue of files found in the input folder.
        /// <div>used for caching</div>
        /// </summary>
        private readonly Queue<string> _inputFilesQueue = new Queue<string>();

        /// <summary>
        /// the name of the listener
        /// </summary>
        private readonly string _name;

        /// <summary>
        /// The folder used to pickup files from. the input folder.
        /// </summary>
        private readonly string _pickupFolder;

        /// <summary>
        /// used for synchronization of stop and start methods
        /// </summary>
        private readonly object _startStopSyncRoot = new object();

        /// <summary>
        /// The folder used to proccess picked up item, before it is removed by client.
        /// </summary>
        //private readonly string _workFolder;

        /// <summary>
        /// flag to indicate if the listener should execute.
        /// </summary>
        private bool _keepAlive = true;

        /// <summary>
        /// The thread that will execute the listener.
        /// </summary>
        private Thread _listenerThreads;

        #endregion

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pickupFolder"></param>
        /// <param name="filter"></param>
        /// <param name="depth"></param>
        public FolderListener(string pickupFolder, string filter, int depth)
        {
            _name = Path.GetDirectoryName(pickupFolder);

            // root the input path if not rooted
            _fileFilter = filter;
            _pickupFolder = Utils.ConvertToFullPath(pickupFolder);
         _directoryDepth = depth;

            // create folder if not exist
            Directory.CreateDirectory(_pickupFolder);

        }

        public bool Enabled { get { return _keepAlive; } }

        /// <summary>
        /// The folder used to pickup files from. the input folder.
        /// </summary>
        public string PickupFolder
        {
            get { return _pickupFolder; }
        }

        /// <summary>
        /// Raised when a file is received on the folder listener.
        /// </summary>
        /// <remarks>
        /// The event is raised on the listener inner thread.
        /// </remarks>
        public event Action<FolderListener, ReceivedFileInfo> FileReceived;

        /// <summary>
        /// Start listener.
        /// </summary>
        /// <exception cref="InvalidOperationException">Listener already started
        /// <div>No subscribers for FileReceived event</div></exception>
        public void StartAsync()
        {
            lock (_startStopSyncRoot)
            {
                if (_listenerThreads != null)
                    throw new InvalidOperationException("Listener already started");

                if (FileReceived == null)
                    throw new InvalidOperationException("No subscribers for FileReceived event");

                Console.WriteLine("Wating for files under {0}", _pickupFolder);
                _listenerThreads = new Thread(ListenerLoop);
                _listenerThreads.Name = _name;
                _listenerThreads.IsBackground = true;
                _listenerThreads.Start();
            }
        }

        /// <summary>
        /// Stop the listener.
        /// </summary>
        /// <remarks>
        /// The stop first attempts to stop gracefully for a time out time (5000msc).
        /// If the listener doesn't stop in that time abort is called.
        /// </remarks>
        public void Stop()
        {
            lock (_startStopSyncRoot)
            {
                if (_listenerThreads != null)
                {
                     Thread[] disposeThreads = { _listenerThreads };
                    _listenerThreads = null;
                    foreach (Thread thread in disposeThreads)
                    {
                        if (thread.ThreadState != ThreadState.Stopped)
                        {
                            Console.WriteLine("Stop listener... [{0}]", _name);
                            _keepAlive = false;
                            thread.Interrupt();
                            bool stopped = thread.Join(STOP_TIMEOUT);

                            if (!stopped)
                            {
                                Console.WriteLine("Listener did not stop in time [{0}], call abort...", STOP_TIMEOUT);
                                thread.Abort();
                            }
                        }
                    }
                }
            }
        }

        #region IDisposable Members

        /// <summary>
        /// stop the listener.
        /// <div>same as calling stop</div>
        /// </summary>
        public void Dispose()
        {
            Stop();
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Listener working thread loop.
        /// </summary>
        private void ListenerLoop()
        {
            try
            {
                while (_keepAlive)
                {
                    try
                    {
                        //Console.WriteLine("Try receive file...");
                        FileInfo receivedFile = TryReceive();

                        if (receivedFile != null)
                        {
                            //Console.WriteLine("File received... [{0}]", receivedFile.FullName);
                            var file = new ReceivedFileInfo(receivedFile);
                            FileReceived(this, file);

                            if (file.DeleteOnComplete && file.Exists)
                            {
                                try
                                {
                                    file.Remove();
                                }
                                catch (Exception ex)
                                {
                                    Console.WriteLine("Failed to delete file after 'FileReceived' event [{0}]", ex);
                                }
                            }
                        }
                        else
                        {
                            Thread.Sleep(BLOCKING_SLEEP_TIME);
                        }
                    }
                    catch (ThreadInterruptedException)
                    {
                        Console.WriteLine("Listener interrupted... [KeepAlive: {0}]", _keepAlive);
                    }
                    catch (IOException ex)
                    {
                        Console.WriteLine("Failed to read file [{0}]", ex.Message);
                    }
                    catch (ThreadAbortException)
                    {
                        throw;
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("Listener failed [{0}]", ex);
                    }
                }
            }
            catch (ThreadAbortException)
            {
                Console.WriteLine("Listener was aborted");
            }
        }

        /// <summary>
        /// Get the next file of the input queue.
        /// </summary>
        /// <remarks>
        /// Block until input file exist or thread is interrupted.
        /// </remarks>
        /// <returns>file info to the received file</returns>
        /// <exception cref="ThreadInterruptedException">if the thread was interrupted while
        /// waiting for input files</exception>
        private FileInfo TryReceive()
        {
            // if the queue is empty, get input files in the input folder
            if (_inputFilesQueue.Count <= 0)
            {
                Thread.Sleep(BLOCKING_SLEEP_TIME);
                QueueInputFolderFiles();
            }

            //Console.WriteLine("Get queued file...");
            string receiveFilePath = _inputFilesQueue.Dequeue();

            // cope with stale files (deleted while waiting in the queue)
            while (!CheckFile(receiveFilePath))
            {
                Console.WriteLine("Stale queued file found, move to next in the queue...");
                if (_inputFilesQueue.Count > 0)
                {
                    receiveFilePath = _inputFilesQueue.Dequeue();
                }
                else
                {
                    receiveFilePath = null;
                    break;
                }
            }

            // check that we found a good file
            if (receiveFilePath == null)
            {
                Console.WriteLine("All files in the queue were stale!, retry receive...");
                return null;
            }

            //Console.WriteLine("Move file to working folder...");

            //get relative path
            //string relativePath = receiveFilePath.Substring(_pickupFolder.Length + 1);

            //string workFilePath = Path.Combine(_workFolder, relativePath);
            //string workFileDirectoryPath = Path.GetDirectoryName(workFilePath);

            //if (!Directory.Exists(workFileDirectoryPath))
            //{
            //    Directory.CreateDirectory(workFileDirectoryPath);
            //}

            //if (File.Exists(workFilePath))
            //{
            //    Console.WriteLine("File already exist, rename file...");
            //    File.Move(workFilePath,
            //              string.Format("{0}.{1}.bak", workFilePath, Guid.NewGuid().ToString().Substring(0, 6)));
            //}

            //File.Move(receiveFilePath, workFilePath);

            //Console.WriteLine("Return received file...");
            return new FileInfo(receiveFilePath);
        }

        /// <summary>
        /// Get files from input folder and enqueue them to the listener inner queue.
        /// </summary>
        /// <remarks>
        /// Block until input file exist or thread is interrupted.
        /// </remarks>
        /// files path are added to the queue, the files still exist in the file system.
        /// <exception cref="ThreadInterruptedException">if the thread was interrupted
        /// while waiting for input files</exception>
        private void QueueInputFolderFiles()
        {
             ICollection<string> files = FileSystemUtils.GetFiles(_pickupFolder, _directoryDepth, _fileFilter, 100000);
            if (files.Count <= 0)
            {
                 while (files.Count <= 0)
                {
                    Thread.Sleep(BLOCKING_SLEEP_TIME);
                    files = FileSystemUtils.GetFiles(_pickupFolder, _directoryDepth, _fileFilter, 1000);
                }
            }

            foreach (string file in files)
            {
                _inputFilesQueue.Enqueue(file);
            }
        }

        /// <summary>
        /// Check queued file that it exist and is not locked.
        /// </summary>
        /// <param name="receiveFilePath">file path to check</param>
        /// <returns>true-all ok, false-not exist or locked</returns>
        private static bool CheckFile(string receiveFilePath)
        {
            if (!File.Exists(receiveFilePath)) return false;

            try
            {
                using (File.OpenRead(receiveFilePath))
                {
                }
            }
            catch (Exception)
            {
                // the file is locked
                return false;
            }

            // file exist and it's not locked
            return true;
        }

        #endregion

    }
}